﻿using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Workflow.Runtime;
using System.Threading;
using CommonWFLibrary;
using System.IO;

namespace CommonWFLibraryTest
{
    /// <summary>
    /// Summary description for t_ExternalProgramActivity
    /// </summary>
    [TestClass]
    public class t_ExternalProgramActivity
    {
        public t_ExternalProgramActivity()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        #region Additional test attributes
        //
        // You can use the following additional attributes as you write your tests:
        //
        // Use ClassInitialize to run code before running the first test in the class
        // [ClassInitialize()]
        // public static void MyClassInitialize(TestContext testContext) { }
        //
        // Use ClassCleanup to run code after all tests in a class have run
        // [ClassCleanup()]
        // public static void MyClassCleanup() { }
        //
        // Use TestInitialize to run code before running each test 
        // [TestInitialize()]
        // public void MyTestInitialize() { }
        //
        // Use TestCleanup to run code after each test has run
        // [TestCleanup()]
        // public void MyTestCleanup() { }
        //
        #endregion

        /// <summary>
        /// Run a test, throw exceptions back to user.
        /// </summary>
        /// <param name="wfActivity"></param>
        /// <param name="dict"></param>
        private static Dictionary<string, object> RunWFByType(Type wfActivity, Dictionary<string, object> dict)
        {
            using (WorkflowRuntime runtime = new WorkflowRuntime())
            using (AutoResetEvent reset = new AutoResetEvent(false))
            {
                Exception exp = null;
                Dictionary<string, object> results = null;
                runtime.WorkflowCompleted += (o, args) => { results = args.OutputParameters; reset.Set(); };
                runtime.WorkflowTerminated += (o, args) => { exp = args.Exception; reset.Set(); };

                LongRunningActivityBase.RegisterService(runtime);
                runtime.StartRuntime();

                WorkflowInstance instance;
                instance = runtime.CreateWorkflow(wfActivity, dict);
                instance.Start();
                bool result = reset.WaitOne(15*1000);

                if (exp != null)
                {
                    throw exp;
                }

                return results;
            }
        }

        /// <summary>
        /// Run a test, throw exceptions back to user. Only, once we go into an idle state, then
        /// crash the WF and restart it. And hook up all the required persistancy!
        /// </summary>
        /// <param name="wfActivity"></param>
        /// <param name="dict"></param>
        /// <returns>A list of the WF public properties upon finish up!</returns>
        private static Dictionary<string,object> RunWFByTypeWithPersistance(Type wfActivity, Dictionary<string, object> dict, int timeout, DirectoryInfo cache, int numberCrashToIdles, int ticksToPauseInbetween)
        {
            ///
            /// Crash the runtime n times.
            /// 

            bool start_wf = true;
            bool has_entered_idle = false;
            Dictionary<string, object> results = null;
            while (numberCrashToIdles >= 0)
            {
                numberCrashToIdles = numberCrashToIdles - 1;
                bool waitForComplete = numberCrashToIdles < 0;

                ///
                /// Run until the WF enters the idle state. If it crashes, then, of course, we are outta here.
                /// 

                using (AutoResetEvent reset = new AutoResetEvent(false))
                using (WorkflowRuntime runtime = new WorkflowRuntime())
                {
                    try
                    {
                        Exception e = null;
                        bool completed = false;
                        if (!waitForComplete)
                        {
                            runtime.WorkflowIdled += (o, args) => { reset.Set(); has_entered_idle = true; };
                        }
                        runtime.WorkflowCompleted += (o, args) => { completed = true; results = args.OutputParameters; reset.Set(); };
                        runtime.WorkflowTerminated += (o, args) => { e = args.Exception; reset.Set(); };

                        runtime.AddService(new FilePersistenceService(true, cache));
                        LongRunningActivityBinaryPersister lrp = new LongRunningActivityBinaryPersister(new FileInfo(cache.FullName + "\\longrunning"));
                        LongRunningActivityBase.RegisterService(runtime, obj => lrp.Save(obj), () => lrp.Restore());
                        runtime.StartRuntime();

                        ///
                        /// If this is the first time through, then we should start the WF. Otherwise, it
                        /// should have been picked up by the persitancy service from the last run.
                        /// 

                        if (start_wf)
                        {
                            WorkflowInstance instance;
                            instance = runtime.CreateWorkflow(wfActivity, dict);
                            instance.Start();
                            start_wf = false;
                        }

                        ///
                        /// Great. Now wait for it to finish. We expect the WF to enter
                        /// an idled state within 1 second. On the last one, waiting for something
                        /// real to happen, we expect that to happen in the timeout parameter passed
                        /// to us.
                        /// 
                        /// We shut down the runtime here so that we can step through the other statements in the
                        /// debugger!
                        /// 
                        /// We wait for 100 ms after the idle guy goes because we have to make sure
                        /// that the service has time to close and save state. In real life, there is
                        /// a potential race condition - that is, if the wf crashes before our system has a chance
                        /// to write out its state then the long running WF will, indeed, be left hanging. But that
                        /// is proper behavior. Just not for a test harness.
                        /// 

                        int waittime = waitForComplete ? timeout : 1000;
                        bool result = reset.WaitOne(waittime);
                        Thread.Sleep(100);
                        runtime.StopRuntime();
                        runtime.Dispose();

                        ///
                        /// First, if the WF ended due to an exception, we are outta here no matter what
                        /// 

                        if (e != null)
                        {
                            throw e;
                        }

                        ///
                        /// Next, if we are waiting for a complete, then we just return the result
                        /// 

                        if (waitForComplete)
                        {
                            return results;
                        }

                        ///
                        /// Ok -- we were waiting for things to go into the idle state. Bomb if they didn't,
                        /// otherwise we go around again!
                        /// 

                        if (completed)
                        {
                            throw new Exception("The WF unexpectedly completed before going idle!");
                        }
                        if (!result && !has_entered_idle)
                        {
                            throw new Exception("The workflow was never idled");
                        }

                        ///
                        /// Pause before the next iteration if requested...
                        /// 

                        if (ticksToPauseInbetween > 0)
                        {
                            Thread.Sleep(ticksToPauseInbetween);
                        }
                    }
                    finally
                    {
                        if (runtime.IsStarted)
                        {
                            runtime.StopRuntime();
                        }
                    }
                }
            }
            return null;
        }


        [TestMethod]
        public void TestSimpleRun()
        {
            /// Run the external program
            Assert.IsTrue(null != RunWFByType(typeof(ExternalProgramActivity), new Dictionary<string, object> { { "ProgramPath", "RunAndPauseWithArg.exe" } }), "Failed to get program return!");
        }

        [TestMethod]
        public void TestSimpleRunBadPath()
        {
            try
            {
                /// Make sure the exception comes back when we can't find the program we are trying to run!
                RunWFByType(typeof(ExternalProgramActivity), new Dictionary<string, object> { { "ProgramPath", "RunAndPauseWithArgFreak.exe" } });
                Assert.Fail("Should have thrown exception because the path of the program we are trying to run does not exist!");
            }
            catch (Exception)
            {
            }
        }

        [TestMethod]
        public void TestRunArgs()
        {
            /// Run external guys with argument string - make sure it gets there!
            DateTime start = DateTime.Now;

            RunWFByType(typeof(ExternalProgramActivity),
                new Dictionary<string, object> {
                { "ProgramPath", "RunAndPauseWithArg.exe" },
                { "ProgramArguments", "3000" }
                });

            DateTime finish = DateTime.Now;
            TimeSpan delta = finish - start;
            Assert.IsTrue(delta.Seconds >= 3, "The wait was supposed to be more than 3 seconds, but was not!");
            Assert.IsTrue(delta.Seconds <= 4, "Should have waited less than 4 seconds!");
        }

        [TestMethod]
        public void TestRunResult()
        {
            /// Run external program, make sure its result is saved
            var results = RunWFByType(typeof(ExternalProgramActivity),
                new Dictionary<string, object> {
                { "ProgramPath", "RunAndPauseWithArg.exe" },
                { "ProgramArguments", "50 5" }
                });

            Assert.IsTrue(results.ContainsKey("FinalStatus"), "The final status property did not come back!");
            Assert.IsTrue((int) results["FinalStatus"] == 5, "Final status was not 5!");
        }

        [TestMethod]
        public void TestRunHostCrashWhileProgramRunning()
        {
            DateTime start = DateTime.Now;
            var results = RunWFByTypeWithPersistance(
                typeof(ExternalProgramActivity),
                new Dictionary<string, object> { { "ProgramPath", "RunAndPauseWithArg.exe" }, { "ProgramArguments", "5000" } },
                6000,
                new DirectoryInfo(".\\TestRunHostCrashWhileProgramRunning"),
                1,
                2000);
            DateTime finish = DateTime.Now;
            TimeSpan delta = finish - start;

            Assert.IsTrue(results != null, "The WF failed to complete!");
            Assert.IsTrue(delta.Seconds >= 5, "The WF should have taken at least 5 seconds -- but took less!?");
            Assert.IsTrue(delta.Seconds <= 6, "The WF shoudl not have taken more than 6 seconds -- something is wrong!");
        }

        [TestMethod]
        public void TestRunHostCrashNoRestart()
        {
            ///
            /// Crash the host, but set the "restart" value to false (normally it is true).
            /// 

            try
            {
                var results = RunWFByTypeWithPersistance(
                    typeof(ExternalProgramActivity),
                    new Dictionary<string, object> { { "ProgramPath", "RunAndPauseWithArg.exe" }, { "ProgramArguments", "500" }, { "AllowRestart", false } },
                    2000,
                    new DirectoryInfo(".\\TestRunHostCrashWhileProgramRunning"),
                    1,
                    2000);
                Assert.Fail("Run should have thrown an exception that we couldn't restart the program");
            }
            catch (AssertFailedException e)
            {
                throw e;
            }
            catch (Exception e)
            {
                Assert.IsTrue(e.InnerException.GetType() == typeof(ExternalProgramActivityException), "Incorrect exception type came back");
                Assert.IsTrue(e.InnerException.Message.Contains("Program crashed"), "Exception was not correct");
            }
        }

        [TestMethod]
        public void TestRunHostCrashRestart()
        {
            /// Have the program crash while the WF host is down. So it should be re-run.
            var results = RunWFByTypeWithPersistance(
                typeof(ExternalProgramActivity),
                new Dictionary<string, object> { { "ProgramPath", "RunAndPauseWithArg.exe" }, { "ProgramArguments", "500" } },
                6000,
                new DirectoryInfo(".\\TestRunHostCrashWhileProgramRunning"),
                1,
                2000);

            Assert.IsTrue(results != null, "The WF failed to complete!");
        }
    }
}
